package framework;

import java.awt.Graphics2D;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * Framework class for game levels
 * 
 * @author Michael Deng
 * 
 */
public abstract class Level {

    private Map<String, List<Sprite>> mySprites;
    private List<Collision> myCollisions;
    private Game myGame;
    private Factory<Sprite> mySpriteFactory;
    private Factory<Collision> myCollisionFactory;
    private Map<Sprite, String> mySpritesToDelete;
    private List<Sprite> mySpritesToBeAdded;
    private String myGamePackage;

    /**
     * Initializes the level, called by the game
     * 
     * @param g reference to the game
     */
    public void initialize (Game g) {
        myGame = g;
        myGamePackage = g.getGamePackage();
        mySprites = new HashMap<String, List<Sprite>>();
        myCollisions = new ArrayList<Collision>();
        mySpritesToDelete = new HashMap<Sprite, String>();
        mySpritesToBeAdded = new ArrayList<Sprite>();

        mySpriteFactory = new Factory<Sprite>(myGamePackage + ".sprite");
        myCollisionFactory = new Factory<Collision>(myGamePackage + ".collision");

        initializeLevel();
        loadLevelObjects();
    }

    /**
     * programmer defined function to initialize anything
     * the level may need before the level runs
     * this can be used for initializing the splash screen if the
     * level is a splash screen
     */
    protected abstract void initializeLevel ();

    /**
     * user defined function to test for the winning
     * condition in the game.
     * 
     * @return if the level is won, returns int specifying the next level, else
     *         returns -1
     */
    protected abstract int testWinCondition ();

    /**
     * 
     * @return if the leve is lost, reutrns int specifying the next level, else
     *         returns -1
     */
    protected abstract int testLoseCondition ();

    /**
     * any behavior that the level executes during each
     * update cycle
     */
    protected abstract void levelBehavior ();

    /**
     * Responsible for initializing all the sprites and collisions for the level
     */
    protected abstract void loadLevelObjects ();

    /**
     * Updates the level
     */
    protected void update () {
        testEndLevel();
        // update all sprites
        for (String i : mySprites.keySet()) {
            for (Sprite s : mySprites.get(i)) {
                s.update();
            }
        }
        // update all collisions
        for (Collision c : myCollisions) {
            c.update();
        }
        // executes the level-wide behavior
        levelBehavior();
        flushDeletedSprites();
        flushSpritesToBeAdded();
    }

    /**
     * Paints all the objects in the level
     * 
     * @param pen used to paint
     */
    protected void paint (Graphics2D pen) {
        for (String i : mySprites.keySet()) {
            for (Sprite s : mySprites.get(i)) {
                s.paint(pen);
            }
        }
    }

    private void testEndLevel () {
        int winResult = testWinCondition();
        int loseResult = testLoseCondition();
        if (winResult > 0) {
            getMyGame().initializeNextLevel(winResult);
        }
        else if (loseResult > 0) {
            getMyGame().initializeNextLevel(loseResult);
        }
    }

    /**
     * Loads the sprite using factory, initialize, and add to map
     * 
     * @param type type of sprite to create
     * @param position where to place the new sprite
     */
    public void loadSprite (String type, Point position) {
        Sprite s = mySpriteFactory.createObject(type);
        if (s != null) {
            s.initialize(position, this);

            if (!mySprites.containsKey(type)) {
                List<Sprite> spriteList = new ArrayList<Sprite>();
                mySprites.put(type, spriteList);
            }

            mySprites.get(type).add(s);
        }
    }

    /**
     * Deletes the referenced sprite from the list of sprites
     * 
     * @param s reference to the sprite
     * @param type type of the sprite to delete
     */
    public void markSpriteToDelete (Sprite s, String type) {
        mySpritesToDelete.put(s, type);
    }

    private void flushDeletedSprites () {
        for (Sprite i : mySpritesToDelete.keySet()) {
            mySprites.get(mySpritesToDelete.get(i)).remove(i);
        }
        mySpritesToDelete.clear();
    }

    /**
     * use this method to load a collision into the level
     * 
     * @param name name of the collision
     */
    protected void loadCollision (String name) {
        Collision c = myCollisionFactory.createObject(name);
        if (c != null) {
            c.initialize(mySprites, this);
            myCollisions.add(c);
        }
    }

    /**
     * 
     * @return a pointer to the game object
     */
    public Game getMyGame () {
        return myGame;
    }

    /**
     * Allows other classes to access the list of sprites
     * 
     * @return list of sprites
     */
    public Map<String, List<Sprite>> getMySprites () {
        return mySprites;
    }

    /**
     * Adds the sprite parameter to the list of sprites to be added at the end
     * of the current "turn."
     * 
     * @param s
     */
    public void markSpriteToBeAdded (Sprite s) {
        mySpritesToBeAdded.add(s);
    }

    /**
     * Adds the sprites in mySpritesToBeAdded to the level (i.e. to mySprites)
     */
    private void flushSpritesToBeAdded () {
        for (Sprite s : mySpritesToBeAdded) {
            addSprite(s);
        }
        mySpritesToBeAdded.clear();
    }

    /**
     * Adds a sprite to the level
     * 
     * @param s Sprite to be added
     */
    public void addSprite (Sprite s) {
        String spriteName = s.getMyName();
        if (!mySprites.containsKey(spriteName)) {
            mySprites.put(spriteName, new ArrayList<Sprite>());
        }
        mySprites.get(spriteName).add(s);
    }

}
